﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

using HtmlAgilityPack;

namespace AWLoadTesterLibrary
{
    /// <summary>
    /// A general purpose class for keeping methods used in the user's test session.
    /// <para>
    /// These are usually going to be attached to particular user or a particular user's browse session.
    /// </para>
    /// </summary>
    /// <remarks>
    /// This class is not threadsafe and so should generally be kept on a single thread.
    /// </remarks>
    public class LoadTesterUtilsClass
    {
        /// <summary>
        /// The maximum number of milliseconds to pause between requests.
        /// <para>The system will choose a random number between this and RandomSleepMin value and pause for that amount of time.</para>
        /// </summary>
        /// <seealso cref="RandomSleepMin"/>
        public int RandomSleepMax { get; set; }

        /// <summary>
        /// The minimum number of milliseconds to pause between requests
        /// </summary>
        /// <seealso cref="RandomSleepMax"/>
        public int RandomSleepMin { get; set; }
        public Random CurrentRandom { get; set; }

        /// <summary>
        /// Cookie store for the current browse session.
        /// </summary>
        public CookieContainer MainCookieContainer { get; set; }

        /// <summary>
        /// The UserAgent string sent to the user during testing.
        /// </summary>
        public string UserAgent { get; set; }

        /// <summary>
        /// Username used for protected items.
        /// </summary>
        public string Username { get; set; }

        /// <summary>
        /// Password used for protected items
        /// </summary>
        public string Password { get; set; }
        public string CurrentUrl { get; set; }

        /// <summary>
        /// Keeps count of the number of requests made in the current browse session
        /// </summary>
        public int RequestCount { get; set; }

        /// <summary>
        /// Indicates the level of tracing and debugging used in the current browse session.
        /// </summary>
        public int DebugLevel { get; set; }

        /// <summary>
        /// Keeps the number of seconds that we may pause between requests.
        /// </summary>
        public int SleepAfterEveryRequest { get; set; }

        /// <summary>
        /// The last response this object made.
        /// </summary>
        public HttpWebResponse LastResponse { get; set; }


        /// <summary>
        /// Create a new Utility class for a browse session
        /// </summary>
        public LoadTesterUtilsClass()
        {
            CurrentRandom = new Random();

            RandomSleepMin = 0;
            RandomSleepMax = 10;

            UserAgent = "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)";
            MainCookieContainer = new CookieContainer();

            Username = "";
            Password = "";

            RequestCount = 0;
            DebugLevel = 0;
            SleepAfterEveryRequest = 1;
        }

        /// <summary>
        /// Pause execution for a random amount of time.
        /// </summary>
        public void RandomSleep()
        {

            if (RandomSleepMax > 10000)
            {
                RandomSleepMax = 10000;
            }

            if (RandomSleepMin > RandomSleepMax)
            {
                RandomSleepMax = RandomSleepMin;
            }

            int rnd = CurrentRandom.Next(RandomSleepMin, RandomSleepMax);

            System.Threading.Thread.Sleep(rnd);
        }

        /// <summary>
        /// Starts the download of a specified URL using a HTTP POST Action.
        /// </summary>
        /// <param name="currUri">The URL to access</param>
        /// <param name="content">The contect to POST to the specified URL</param>
        /// <returns>The HttpWebRequest resulting from the POST</returns>
        public HttpWebRequest GetPostRequest(Uri currUri, string content)
        {
            HttpWebRequest req = null;

            req = (HttpWebRequest)WebRequest.Create(currUri);

            req.Method = "POST";
            req.PreAuthenticate = true;

            if (String.IsNullOrWhiteSpace(Username) == false)
            {
                req.Credentials = new NetworkCredential(Username, Password);
            }

            req.CookieContainer = MainCookieContainer;
            req.ServicePoint.Expect100Continue = false;
            req.UserAgent = UserAgent;
            req.ContentType = "application/x-www-form-urlencoded";

            req.ContentLength = content.Length;
            using (StreamWriter sw = new StreamWriter(req.GetRequestStream(), Encoding.ASCII))
            {
                sw.Write(content);
            }

            RequestCount++;

            if (DebugLevel > 0)
            {
                System.Diagnostics.Trace.WriteLine(String.Format("Sending... {0}\r\n{1}\r\n\r\n=====", currUri.ToString(), content));
            }

            return req;
        }

        /// <summary>
        /// Returns the HtmlAgilityPack document from an already open HttpWebRequest
        /// </summary>
        /// <param name="req">The request that has already been started</param>
        /// <returns>HtmlAgilityPack document object that represents the parsed HTML response</returns>
        public HtmlDocument GetResponseDocument(HttpWebRequest req)
        {
            HtmlDocument ret = null;

            try
            {
                LastResponse = (HttpWebResponse)req.GetResponse();
                MainCookieContainer.Add(LastResponse.Cookies);
            }
            catch (WebException ex)
            {
                throw new LoadTesterException(
                    String.Format("Connection to '{0}' failed", CurrentUrl), ex);
            }

            string resStreamStr = null;
            using (StreamReader resStream = new StreamReader(LastResponse.GetResponseStream()))
            {
                resStreamStr = resStream.ReadToEnd();
            }

            if (String.IsNullOrEmpty(resStreamStr))
            {
                throw new LoadTesterException(
                    String.Format("Unexpected empty result from {0}", CurrentUrl));
            }

            if (DebugLevel > 0)
            {
                System.Diagnostics.Trace.WriteLine(String.Format("Receiving... {0}\r\n{1}\r\n\r\n=====", req.RequestUri.ToString(), resStreamStr));
            }

            ret = new HtmlDocument();
            ret.LoadHtml(resStreamStr);

            // Throttle if necessary
            if (SleepAfterEveryRequest > 0)
            {
                RandomSleep();
            }

            return ret;
        }

        /// <summary>
        /// Returns the HtmlAgilityPack document object response from a specified test URL after using HTTP POST Action to send it content.
        /// </summary>
        /// <param name="currUri">The test URL</param>
        /// <param name="content">The content to POST to the test URL</param>
        /// <returns>HtmlAgilityPack document object that represents the parsed HTML response</returns>
        public HtmlDocument GetPostUriDoc(Uri currUri, string content)
        {
            HtmlDocument currDoc = null;

            for (int i = 0; i < 5; i++)
            {
                try
                {
                    currDoc = GetResponseDocument(GetPostRequest(currUri, content));

                    break;
                }
                catch (Exception ex)
                {

                }
            }

            return currDoc;
        }

        /// <summary>
        /// Returns a web request object for a specified URL using HTTP GET Action
        /// </summary>
        /// <param name="currUri">The test URL to access</param>
        /// <returns>An open HttpWebRequest</returns>
        public HttpWebRequest GetGetRequest(Uri currUri)
        {
            HttpWebRequest req = null;

            req = (HttpWebRequest)WebRequest.Create(currUri);

            req.Method = "GET";
            req.PreAuthenticate = true;
            if (String.IsNullOrWhiteSpace(Username) == false)
            {
                req.Credentials = new NetworkCredential(Username, Password);
            }
            req.CookieContainer = MainCookieContainer;
            req.ServicePoint.Expect100Continue = false;
            req.UserAgent = UserAgent;

            RequestCount++;

            return req;
        }

        /// <summary>
        /// Returns the string contents of a HtmlDocument object
        /// </summary>
        /// <param name="doc">The document to convert</param>
        /// <returns>The HTML code</returns>
        public string ToString(HtmlDocument doc)
        {
            string result = "";

            StringWriter sw = new StringWriter();
            doc.Save(sw);

            result = sw.ToString();

            return result;
        }

        /// <summary>
        /// Returm the HtmlDocument that represents the response from a HTTP Get Action to a specified URL
        /// </summary>
        /// <param name="uri">The test URL to accesss</param>
        /// <param name="retryCount">The number of times we will try the test URL before giving up</param>
        /// <returns>HtmlAgilityPack document object that represents the parsed HTML response</returns>
        public HtmlDocument GetResponseDocument(Uri uri, int retryCount)
        {
            HtmlDocument ret = null;

            for (int i = 0; i < retryCount && i < 5; i++)
            {
                try
                {
                    HttpWebRequest req = GetGetRequest(uri);
                    ret = GetResponseDocument(req);

                    break;
                }
                catch (LoadTesterException ex)
                {
                    if (ex.InnerException != null)
                    {
                        WebException wex = ex.InnerException as WebException;
                        if (wex != null)
                        {
                            HttpWebResponse res = wex.Response as HttpWebResponse;
                            if (res != null && res.StatusCode == HttpStatusCode.NotFound)
                            {
                                // Don't bother retrying 404s
                                break;
                            }
                        }
                    }
                }
                catch (WebException ex)
                {
                    ;
                }
                catch (Exception ex)
                {
                    throw;
                }
            }

            //if (ret == null)
            //    throw new LoadTesterException(
            //        String.Format("Error retrieving '{0}'",
            //                        uri.ToString()));

            return ret;
        }

        /// <summary>
        /// Parses and returns links from CSS
        /// </summary>
        /// <param name="CSSStr">A string representing CSS</param>
        /// <returns>A list of links found in the CSS</returns>
        public List<LoadTesterLink> GetLinksFromCSS(string CSSStr)
        {
            List<LoadTesterLink> resLink = new List<LoadTesterLink>();

            if (String.IsNullOrWhiteSpace(CSSStr))
            {
                return resLink;
            }

            Regex reg = new Regex(@"background[-\w]*[\s]*:.*url[\s]*\(['""]*([\)'""]+)");

            foreach (Match currMatch in reg.Matches(CSSStr))
            {
                resLink.Add(new LoadTesterLink() { LinkString = currMatch.Groups[1].Value, LinkType = LoadTesterLinkType.IMG });
            }

            return resLink;
        }

    }

    /// <summary>
    /// General application exceptions generated by this library
    /// </summary>
    public class LoadTesterException : ApplicationException
    {
        /// <summary>
        /// Create a new exception
        /// </summary>
        public LoadTesterException()
            : base()
        {

        }

        /// <summary>
        /// Create a new exception
        /// </summary>
        /// <param name="msg">Include a message</param>
        public LoadTesterException(string msg)
            : base(msg)
        {

        }

        /// <summary>
        /// Create a new exception
        /// </summary>
        /// <param name="msg">Include a message</param>
        /// <param name="msg">Inner exception</param>
        public LoadTesterException(string msg, Exception ex)
            : base(msg,ex)
        {

        }
    }

    /// <summary>
    /// Types of links
    /// </summary>
    public enum LoadTesterLinkType
    {
        None,
        LINK_CSS,
        LINK_HREF,
        SCRIPT,
        IMG
    }

    /// <summary>
    /// Describes a link in a document
    /// </summary>
    public class LoadTesterLink
    {
        /// <summary>
        /// The type of this link
        /// </summary>
        public LoadTesterLinkType LinkType { get; set; }

        /// <summary>
        /// The link's original string representation
        /// </summary>
        public string LinkString { get; set; }

        /// <summary>
        /// Current link Uri
        /// </summary>
        public Uri LinkUri { get; set; }

        /// <summary>
        /// Updates the 'Uri' member with the 'LinkString'
        ///
        /// </summary>
        /// <param name="relatedUri">A relate URL</param>
        public void UpdateUri(Uri relatedUri)
        {
            LinkUri = new Uri(LinkString,UriKind.RelativeOrAbsolute);
            if (LinkUri.IsAbsoluteUri==false)
            {
                //LinkUri = new Uri(relatedUri.GetLeftPart(UriPartial.Authority) + LinkString);
                LinkUri = new Uri(relatedUri, LinkUri);
            }
        }

    }
}
